<!DOCTYPE html>
<html xmlns="http://www.w3.org/1999/xhtml" lang="en">
<head>
	<meta charset="utf-8" />
	<script type="text/javascript">//<![CDATA[
		"use strict"; (function () { var vp = document.createElement("meta"); vp.setAttribute("name", "viewport"); vp.setAttribute("content", ((screen.availWidth >= 544) || (screen.availHeight >= 544)) ? "width=device-width, initial-scale=1" : "width=544"); document.head.appendChild(vp); })(); //]]>
	</script>
	<link rel="Shortcut Icon" href="/favicon.ico" />
	<title>Graphical Filter Editor (Test)</title>
	<style type="text/css">
		body {
			margin: 16px;
			padding: 0px;
			background-color: #000;
			color: #fff;
		}
		body, input, button, select {
			font: normal 16px 'Segoe UI', sans-serif;
		}
		p {
			font-size: 12px;
			color: #999;
		}
		code {
			font-family: Consolas, monospace;
		}
		a {
			color: #0f2;
		}
		a:active, a:visited {
			color: #f33;
		}
		select, input, button, label, img {
			vertical-align: middle;
		}
		.SEP {
			margin: 8px 0px;
			border-top: 1px solid #fff;
		}
		.HID {
			display: none;
		}
	</style>
	<link rel="stylesheet" href="GraphicalFilterEditor.css" />
	<script type="text/javascript" charset="utf-8" src="Common.js"></script>
	<script type="text/javascript" charset="utf-8" src="FFTNR.js"></script>
	<script type="text/javascript" charset="utf-8" src="GraphicalFilterEditor.js"></script>
	<script type="text/javascript" charset="utf-8" src="GraphicalFilterEditorControl.js"></script>
	<script type="text/javascript" charset="utf-8" src="Analyzer.js"></script>
	<script type="text/javascript" charset="utf-8" src="AnalyzerWL.js"></script>
	<script type="text/javascript" charset="utf-8" src="Program.js"></script>
	<script type="text/javascript" charset="utf-8" src="SoundParticles.js"></script>
	<script type="text/javascript">
		//<![CDATA[
		"use strict";
		var audioContext, source, sourceAudio, graphicEqualizer, splitter, analyzer, analyzerType, merger, pendingUrls, chkSource;
		//fakeAudioContext was created only to act as a "null audio context", making at least the graph work in other browsers
		function fakeAudioContext() {
		}
		fakeAudioContext.prototype = {
			sampleRate: 44100,
			createChannelSplitter: function () {
				return {};
			},
			createChannelMerger: function () {
				return {};
			},
			createBufferSource: function () {
				return {};
			},
			createBuffer: function (channels, filterLength, sampleRate) {
				if (sampleRate === undefined)
					return this.createBuffer(2, 1024, this.sampleRate);
				return {
					duration: filterLength / sampleRate,
					gain: 1,
					length: filterLength,
					numberOfChannels: channels,
					sampleRate: sampleRate,
					data: (function () {
						var a = new Array(channels), i;
						for (i = channels - 1; i >= 0; i--)
							a[i] = new Float32Array(filterLength);
						return a;
					})(),
					getChannelData: function (index) { return this.data[index]; }
				};
			},
			createConvolver: function () {
				var mthis = this;
				return {
					buffer: null,
					context: mthis,
					normalize: true,
					numberOfInputs: 1,
					numberOfOutputs: 1
				};
			}
		};
		function main() {
			pendingUrls = [];
			attachMouse($("btnPlay"), "click", play);
			attachMouse($("btnStop"), "click", stop);
			attachMouse($("btnProcess"), "click", processAndDownload);
			chkSource = [$("chkSource0"), $("chkSource1"), $("chkSource2")];
			chkSource[0].addEventListener("change", chkSource_Change);
			chkSource[1].addEventListener("change", chkSource_Change);
			chkSource[2].addEventListener("change", chkSource_Change);
			$("txtFile").addEventListener("change", txtFile_Change);
			$("txtURL").addEventListener("change", txtURL_Change);
			$("cbFilterLength").addEventListener("change", filterLengthChanged);
			$("cbAnalyzer").addEventListener("change", updateConnections);
			audioContext = (window.AudioContext ? new AudioContext() : (window.webkitAudioContext ? new webkitAudioContext() : new fakeAudioContext()));
			graphicEqualizer = new GraphicalFilterEditorControl(2048, audioContext);
			graphicEqualizer.createControl($("equalizerPlaceholder"));
			analyzerType = null;
			analyzer = null;
			splitter = audioContext.createChannelSplitter();
			merger = audioContext.createChannelMerger();
			return true;
		}
		function chkSource_Change() {
			var e = (chkSource[1].checked ? "disabled" : "");
			$("cbLoadType").disabled = e;
			$("btnProcess").disabled = e;
			return true;
		}
		function selectSource(index) {
			chkSource[index].checked = true;
			return chkSource_Change();
		}
		function txtFile_Change() {
			return selectSource(0);
		}
		function txtURL_Change() {
			return selectSource(1);
		}
		function cleanUpAnalyzer() {
			if (analyzer) {
				analyzer.stop();
				analyzer.destroyControl();
			}
			splitter.disconnect(0);
			splitter.disconnect(1);
			if (analyzer) {
				analyzer.analyzerL.disconnect(0);
				analyzer.analyzerR.disconnect(0);
				analyzerType = null;
				analyzer = null;
			}
			merger.disconnect(0);
			return true;
		}
		function enableButtons(enable) {
			var e = (enable ? "" : "disabled");
			$("btnPlay").disabled = e;
			$("btnProcess").disabled = e;
			$("btnStop").disabled = e;
			chkSource[0].disabled = e;
			chkSource[1].disabled = e;
			chkSource[2].disabled = e;
			return true;
		}
		function showLoader(show) {
			$("imgLoader").className = (show ? "" : "HID");
			return true;
		}
		function createObjURL(obj, opts) {
			var url = (window.URL || window.webkitURL), objurl = (opts ? url.createObjectURL(obj, opts) : url.createObjectURL(obj));
			pendingUrls.push(objurl);
			return objurl;
		}
		function freeObjURLs() {
			if (pendingUrls.length) {
				var i, url = (window.URL || window.webkitURL);
				for (i = pendingUrls.length - 1; i >= 0; i--)
					url.revokeObjectURL(pendingUrls[i]);
				pendingUrls.splice(0, pendingUrls.length);
			}
			return true;
		}
		function stop() {
			enableButtons(true);
			if (sourceAudio) {
				sourceAudio.pause();
				sourceAudio = null;
				source.disconnect(0);
				source = null;
			} else if (source) {
				source.stop(0);
				source.disconnect(0);
				source = null;
			}
			graphicEqualizer.filter.convolver.disconnect(0);
			//Free all created URL's only at safe moments!
			freeObjURLs();
			return cleanUpAnalyzer();
		}
		function handleError(e) {
			showLoader(false);
			enableButtons(true);
			//Free all created URL's only at safe moments!
			freeObjURLs();
			alert(e);
			return true;
		}
		function updateConnections() {
			var t = $("cbAnalyzer").value;
			if (!source) return false;
			graphicEqualizer.filter.convolver.disconnect(0);
			switch (t) {
				case "soundParticles":
				case "fft":
				case "wl":
					if (analyzerType !== t) {
						if (analyzer) cleanUpAnalyzer();
						analyzerType = t;
						switch (t) {
							case "soundParticles":
								analyzer = new SoundParticles(audioContext, graphicEqualizer.filter);
								break;
							case "fft":
								analyzer = new Analyzer(audioContext, graphicEqualizer.filter);
								break;
							case "wl":
								analyzer = new AnalyzerWL(audioContext, graphicEqualizer.filter);
								break;
						}
						analyzer.createControl($("analyzerPlaceholder"));
					}

					graphicEqualizer.filter.convolver.connect(splitter, 0, 0);
					splitter.connect(analyzer.analyzerL, 0, 0);
					splitter.connect(analyzer.analyzerR, 1, 0);

					analyzer.analyzerL.connect(merger, 0, 0);
					analyzer.analyzerR.connect(merger, 0, 1);

					merger.connect(audioContext.destination, 0, 0);
					return analyzer.start();
				default:
					graphicEqualizer.filter.convolver.connect(audioContext.destination, 0, 0);
					return cleanUpAnalyzer();
			}
		}
		function filterLengthChanged() {
			graphicEqualizer.changeFilterLength(parseInt($("cbFilterLength").value));
			return true;
		}
		function finishLoadingIntoMemoryAndPlay(array, name, offline) {
			try {
				//Decode the array asynchronously
				audioContext.decodeAudioData(array, function (buffer) {
					try {
						if (offline) {
							//Start processing the decoded buffer offline
							var offlineAudioContext = (window.OfflineAudioContext ? new OfflineAudioContext(buffer.numberOfChannels, buffer.length, buffer.sampleRate) : (window.webkitOfflineAudioContext ? new webkitOfflineAudioContext(buffer.numberOfChannels, buffer.length, buffer.sampleRate) : null));
							if (!offlineAudioContext)
								return handleError("Offline audio processing is not supported!");
							source = offlineAudioContext.createBufferSource();
							source.buffer = buffer;
							source.loop = false;
							graphicEqualizer.changeAudioContext(offlineAudioContext);
							source.connect(graphicEqualizer.filter.convolver, 0, 0);
							graphicEqualizer.filter.convolver.connect(offlineAudioContext.destination, 0, 0);
							source.start(0);
							offlineAudioContext.oncomplete = function (renderedData) {
								var worker = new Worker("WaveExporterWorker.js"),
									leftBuffer = renderedData.renderedBuffer.getChannelData(0).buffer,
									rightBuffer = ((renderedData.renderedBuffer.numberOfChannels > 1) ? renderedData.renderedBuffer.getChannelData(1).buffer : null);
								worker.onmessage = function (e) {
									showLoader(false);
									enableButtons(true);
									//Massive workaround to save the file (simulate a click on a link)!
									//(From: http://updates.html5rocks.com/2011/08/Saving-generated-files-on-the-client-side)
									var a = document.createElement("a"), i = name.lastIndexOf("."), evt;
									a.href = createObjURL(new Blob(e.data, { type: "application/octet-stream" }));
									a.download = ((i > 0) ? (name.substring(0, i) + " - (Filtered).wav") : "FilteredFile.wav");
									//a.click(); //Works on Chrome, but not on Firefox...
									evt = document.createEvent("MouseEvents");
									evt.initMouseEvent("click", true, false, window, 0, 0, 0, 0, 0, false, false, false, false, 0, null);
									a.dispatchEvent(evt);
									return true;
								};
								worker.postMessage({
									left: leftBuffer,
									right: rightBuffer,
									length: renderedData.renderedBuffer.length,
									sampleRate: (renderedData.renderedBuffer.sampleRate | 0)
								}, [ leftBuffer, rightBuffer ]);
								return true;
							};
							offlineAudioContext.startRendering();
						} else {
							//Play the decoded buffer
							source = audioContext.createBufferSource();
							source.buffer = buffer;
							source.loop = true;
							graphicEqualizer.changeAudioContext(audioContext);
							source.connect(graphicEqualizer.filter.convolver, 0, 0);
							updateConnections();
							source.start(0);
							showLoader(false);
							$("btnStop").disabled = "";
						}
					} catch (e) {
						handleError(e);
					}
					return true;
				}, function () {
					return handleError("Error decoding the file!");
				});
			} catch (e) {
				handleError(e);
			}
			return true;
		}
		function loadIntoMemoryAndPlay(offline) {
			var r, f, done = false;
			showLoader(true);
			if (chkSource[2].checked) {
				//Read the sample file into memory
				r = new XMLHttpRequest();
				r.open("GET", "midnightride.mp3", true);
				r.responseType = "arraybuffer";
				r.onreadystatechange = function () {
					if (r.readyState === 4 && !done) {
						done = true;
						finishLoadingIntoMemoryAndPlay(r.response, "Midnight Ride.mp3", offline);
					}
					return true;
				};
				r.send();
			} else {
				f = $("txtFile").files[0];
				//Read the chosen file into memory
				r = new FileReader();
				r.onload = function () {
					done = true;
					finishLoadingIntoMemoryAndPlay(r.result, f.name, offline);
					return true;
				};
				r.onerror = function () {
					return handleError("Error reading the file!");
				};
				r.onloadend = function () {
					if (!offline && !done)
						showLoader(false);
					return true;
				};
				r.readAsArrayBuffer(f);
			}
			return true;
		}
		function prepareStreamingAndPlay() {
			//Chrome now supports processing audio played over streamings (tested with Chrome v29.0.1547.76)
			if (chkSource[0].checked) {
				//If chkSource[0] is checked, create a temporary URL for the chosen file
				sourceAudio = new Audio(createObjURL($("txtFile").files[0]));
			} else if (chkSource[1].checked) {
				sourceAudio = new Audio($("txtURL").value);
			} else {
				sourceAudio = new Audio("midnightride.mp3");
			}
			sourceAudio.loop = true;
			source = audioContext.createMediaElementSource(sourceAudio);
			sourceAudio.load();
			graphicEqualizer.changeAudioContext(audioContext);
			source.connect(graphicEqualizer.filter.convolver, 0, 0);
			updateConnections();
			sourceAudio.play();
			$("btnStop").disabled = "";
			return true;
		}
		function play() {
			if (chkSource[0].checked) {
				if ($("txtFile").files.length === 0) {
					alert("Please, select a file to play!");
					return true;
				}
			} else if (chkSource[1].checked) {
				if ($("txtURL").value.length === 0) {
					alert("Please, type the address of a file to be played :(");
					return true;
				}
			}
			if (!window.AudioContext && !window.webkitAudioContext) {
				alert("Your browser does not seem to support the Web Audio API! :(");
				return true;
			}
			stop();
			enableButtons(false);
			try {
				if (!chkSource[1].checked && parseInt($("cbLoadType").value))
					loadIntoMemoryAndPlay(false);
				else
					prepareStreamingAndPlay();
			} catch (e) {
				handleError(e);
			}
			return true;
		}
		function processAndDownload() {
			if (chkSource[0].checked) {
				if ($("txtFile").files.length === 0) {
					alert("Please, select a file to process!");
					return true;
				}
			} else if (chkSource[1].checked) {
				//We can't load an entire stream into memory... as far as we know, a few URL's
				//point to infinite streams ;)
				alert("Sorry, but that action is not available for streams :(");
				return true;
			}
			if (!window.AudioContext && !window.webkitAudioContext) {
				alert("Your browser does not seem to support the Web Audio API! :(");
				return true;
			}
			if (!window.Worker) {
				alert("Your browser does not seem to support the Web Worker API! :(");
				return true;
			}
			stop();
			enableButtons(false);
			try {
				loadIntoMemoryAndPlay(true);
			} catch (e) {
				handleError(e);
			}
			return true;
		}
		//]]>
	</script>
</head>
<body>
<div style="margin: 16px 0px;"><div style="display:inline-block; background-color: #303030; padding: 8px;">
<input type="radio" value="0" name="chkSource" id="chkSource0" /> <label for="txtFile" onclick="return selectSource(0);">Load your own file:</label> <input type="file" id="txtFile" /><br />
or<br />
<input type="radio" value="1" name="chkSource" id="chkSource1" /> <label for="txtURL" onclick="return selectSource(1);">Enter the address of a file:</label> <input type="url" id="txtURL" /> <span>* Not all URL's may work, and may fail without warning :(</span><br />
or<br />
<input type="radio" value="2" name="chkSource" id="chkSource2" checked="checked" /> <label for="chkSource2" onclick="return selectSource(2);">Try this sample: 50 seconds from the free track Midnight Ride, created by SoundJay</label> - <a href="http://www.soundjay.com">www.soundjay.com</a><br />
<div class="SEP"></div>
<button id="btnPlay">Play</button>
<button id="btnStop">Stop</button>
|
<select id="cbLoadType">
	<option value="0" selected="selected">Play file via streaming *</option>
	<option value="1">Load entire file into memory before playing **</option>
</select>
<div class="SEP"></div>
<button id="btnProcess">Process file offline and download the filtered version (WAVE)! **</button> <img class="HID" id="imgLoader" alt="Processing..." src="loader.gif" />
<div class="SEP"></div>
<label for="cbFilterLength">Filter Length:</label> <select id="cbFilterLength">
	<option value="64">64</option>
	<option value="128">128</option>
	<option value="256">256</option>
	<option value="512">512</option>
	<option value="1024">1024</option>
	<option value="2048" selected="selected">2048</option>
	<option value="4096">4096</option>
	<option value="8192">8192</option>
</select> <label for="cbAnalyzer">Analyzer:</label> <select id="cbAnalyzer">
	<option value="none" selected="selected">None</option>
	<option value="soundParticles">Sound Particles</option>
	<option value="fft">Frequency analyzer</option>
	<option value="wl">Haar wavelet analyzer</option>
</select></div></div>
<div id="equalizerPlaceholder"></div><div id="analyzerPlaceholder" style="vertical-align: top; display: inline-block; margin-left: 16px;"></div>
<p>This work was presented at <a href="http://www.cictem.com.ar/home.html">CICTEM 2013</a>, in Argentina! :D <img alt="CICTEM 2013" src="cictem.png" width="250" height="98" /></p>
<p>Download links for the documentation:<br />
	<a href="presentation.pdf">Presentation</a><br />
	<a href="paper45_ptbr.pdf">Portuguese paper</a><br />
	English paper coming soon!
</p>
<div class="SEP"></div>
<p>This is a test for a JavaScript graphical filter editor, created by me
(Carlos Rafael Gimenes das Neves - <a href="http://twitter.com/carlosrafaelgn">@carlosrafaelgn</a>, <code id="shdg3724"></code>), based on my old C++ graphic equalizer.</p>
<p>Check out the <a href="https://github.com/carlosrafaelgn/GraphicalFilterEditor">source</a> for more information! This test uses <a href="http://www.w3.org/TR/webaudio/">Web Audio API</a>, <a href="http://www.w3.org/TR/FileAPI/">File API</a> and <a href="http://www.w3.org/TR/workers/">Web Worker API</a> and requires a <a href="http://caniuse.com/audio-api">compliant browser</a> to run properly. In <a href="https://wiki.mozilla.org/WebAudio_API_Rollout_Status">Firefox 23 and 24</a>, Web Audio API must be enabled using <code>about:config</code>.</p>
<p>Please, load files with a sample rate of 44100Hz or 48000Hz (the filter itself supports any sample rate, this is just due to AudioContext).</p>
<p>Check out the functions <code>main()</code> and <code>updateConnections()</code> to see how to have stereo output using two analyzers!</p>
<p>If running this locally in Chrome, it must be started with the command-line option <code>--allow-file-access-from-files</code> otherwise you will not be able to load any files!</p>
<p>* Playing files via streaming was tested on Chrome v29.0.1547.76 and later, and on Firefox Nightly v27.0a1 (2013-10-04). If any error happens, or you hear no difference when changing the filter, please, load the entire file into memory before playing.</p>
<p>** Chrome v30 has apparently stopped to support loading large files into memory. If any error happens, or only a small portion of the file is played/filtered, please, use either Chrome v29/v32+ or Firefox v26/v27+.</p>

<script type="text/javascript">//<![CDATA[
	"use strict";
	document.getElementById("shdg3724").appendChild(document.createTextNode(
	["c", "a", "r", "l", "o", "s", "r", "a", "f", "a", "e", "l", ".", "prog"].join("") +
	"\u0040" +
	["g", "m", "a", "i", "l", "\u002e"].join("") +
	["c", "o", "m"].join("")));
	main(); //]]></script>
</body>
</html>
